/* PhotoImport
 * DoDownload class handles download job
 * Copyright by faena.com
 * 2010-08-17 VM created
 * 
*/
using System;
using System.Threading;
using System.IO;
using System.Collections.Generic;

namespace PhotoImport
{
	public class DoDownload
	{
		private int _totalFiles; 		//Total number of files going to be processed
		private long _totalSize; //Total number of bytes going to be processed
		private	long _totalCopied = 0;

		/// <summary>
		/// Nothing to do in constructor 
		/// </summary>
		public DoDownload ()
		{
		}

		/// <summary>
		/// Create subfolder and copy the file into it. 
		/// </summary>
		/// <param name="pSourceFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="pTargetDir">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="pSf">
		/// A <see cref="SettingFile"/>
		/// </param>
		private bool CopyWithSubDir (string pSourceFileName, string pTargetDir, SettingFile pSf)
		{
			bool ret = false;
			string targetDirNew = "";
			//full path of the new target
			string newSubdir = "";
			//1st level of the new subfolder
			string newSubdir2 = "";
			//2nd level of the new subfolder
			string newSubdir3 = "";
			//3rd level of the new subfolder
			DateTime useTime;
			switch (pSf.UseTime) {
			case SettingFile.UseTimes.UseCurrentTime:
				useTime = DateTime.Today;
				break;
			case SettingFile.UseTimes.UseShotTime:
				useTime = System.IO.File.GetCreationTime (pSourceFileName);
				break;
			default:
				throw new Exception (string.Format ("Program error. Unknown UseTime:{0}", pSf.UseTime));
			}
			switch (pSf.SubfolderTemplate) {
			case SettingFile.SubfolderTemplates.YearMonthDate:
				//0: //YearMonthDate
				newSubdir = useTime.ToString ("yyyy_MM_dd");
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + DateTime.Today.ToString ("yyyy_MM_dd");
				break;
			case SettingFile.SubfolderTemplates.NewName:
				//1: 	//New Name
				newSubdir = pSf.NewName;
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + uxNewName.Text;
				
				break;
			case SettingFile.SubfolderTemplates.Year_YearMonthDate:
				//2://Year\YearMonthDate
				newSubdir = useTime.ToString ("yyyy");
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				newSubdir2 = useTime.ToString ("yyyy_MM_dd");
				targetDirNew = System.IO.Path.Combine (targetDirNew, newSubdir2);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + DateTime.Today.ToString ("yyyy") + "\n---" + DateTime.Today.ToString ("yyyy_MM_dd");
				break;
			case SettingFile.SubfolderTemplates.Year_NewName:
				//3://Year\New Name
				newSubdir = useTime.ToString ("yyyy");
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				newSubdir2 = pSf.NewName;
				targetDirNew = System.IO.Path.Combine (targetDirNew, newSubdir2);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + DateTime.Today.ToString ("yyyy") + "\n---" + uxNewName.Text;
				break;
			case SettingFile.SubfolderTemplates.NewName_YearMonthDate:
				// 4://New Name\YearMonthDate
				newSubdir = pSf.NewName;
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				newSubdir2 = useTime.ToString ("yyyy_MM_dd");
				targetDirNew = System.IO.Path.Combine (targetDirNew, newSubdir2);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + uxNewName.Text + "\n--" + DateTime.Today.ToString ("yyyy_MM_dd");
				break;
			case SettingFile.SubfolderTemplates.Year_Month_YearMonthDate:
				// 5:	//Year\Month\YearMonthDate
				newSubdir = useTime.ToString ("yyyy");
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				newSubdir2 = useTime.ToString ("MM");
				targetDirNew = System.IO.Path.Combine (targetDirNew, newSubdir2);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				newSubdir3 = useTime.ToString ("yyyy_MM_dd");
				targetDirNew = System.IO.Path.Combine (targetDirNew, newSubdir3);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + DateTime.Today.ToString ("yyyy") + "\n---" + DateTime.Today.ToString ("MM") + "\n----" + DateTime.Today.ToString ("yyyy_MM_dd");
				break;
			case SettingFile.SubfolderTemplates.Year_Month:
				// 6:			//Year\Month
				newSubdir = useTime.ToString ("yyyy");
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				newSubdir2 = useTime.ToString ("MM");
				targetDirNew = System.IO.Path.Combine (targetDirNew, newSubdir2);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + DateTime.Today.ToString ("yyyy") + "\n---" + DateTime.Today.ToString ("MM");
				break;
			case SettingFile.SubfolderTemplates.YearMonthDate_NewName:
				// 7:	//YearMonthDate\New Name
				newSubdir = useTime.ToString ("yyyy_MM_dd");
				targetDirNew = System.IO.Path.Combine (pTargetDir, newSubdir);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				
				newSubdir2 = pSf.NewName;
				targetDirNew = System.IO.Path.Combine (targetDirNew, newSubdir2);
				if (!System.IO.Directory.Exists (targetDirNew))
					System.IO.Directory.CreateDirectory (targetDirNew);
				//uxExample.Text = "Example:\n-" + uxTargetDir.Text + "\n--" + DateTime.Today.ToString ("yyyy_MM_dd") + "\n--" + uxNewName.Text;
				break;
			default:
				throw new Exception (string.Format ("Program error. Unknown SubfolderTemplate:{0}", pSf.SubfolderTemplate));
			}
			if (System.IO.Directory.Exists (targetDirNew))
				ret = CopyFile (pSourceFileName, targetDirNew, pSf);
			else
				Log (string.Format ("Target dir not exist:{0}\n", targetDirNew), true);
			return ret;
		}

		private const int BUFFER_SIZE = 1024;
		private byte[] _1kBuf0;
		private byte[] _1kBuf1;
		/// <summary>
		/// Check to see if source and target are the same file. Assume both files exist.
		/// </summary>
		/// <param name="pSourceFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="pTargerFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// A <see cref="System.Boolean"/>
		/// </returns>
		private bool IsSameFile (string pSourceFileName, string pTargerFileName)
		{
			//First check the file length
			System.IO.FileInfo sourceFi = new System.IO.FileInfo (pSourceFileName);
			System.IO.FileInfo targetFi = new System.IO.FileInfo (pTargerFileName);
			if (sourceFi.Length != targetFi.Length)
				return false;
			
			//If they are the same length, check content
			bool isSame = true;
			using (System.IO.BinaryReader sourceBr = new System.IO.BinaryReader (new System.IO.FileStream (pSourceFileName, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read))) {
				using (System.IO.BinaryReader targetBr = new System.IO.BinaryReader (new System.IO.FileStream (pTargerFileName, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read))) {
					
					int sourceReadLen = sourceBr.Read (_1kBuf0, 0, BUFFER_SIZE);
					int targetReadLen = targetBr.Read (_1kBuf1, 0, BUFFER_SIZE);
					
					while (sourceReadLen > 0) {
						//Check acutall data length read. These are unlikely to be different.
						if (sourceReadLen != targetReadLen) {
							isSame = false;
							break;
						}
						
						//Compare byte by byte
						for (int i = 0; i < sourceReadLen; i++) {
							if (_1kBuf0[i] != _1kBuf1[i]) {
								isSame = false;
								break;
							}
						}
						
						if (sourceReadLen < BUFFER_SIZE)
							break;
						//file read ends
						sourceReadLen = sourceBr.Read (_1kBuf0, 0, BUFFER_SIZE);
						targetReadLen = targetBr.Read (_1kBuf1, 0, BUFFER_SIZE);
					}
					sourceBr.Close ();
					targetBr.Close ();
				}
			}
			//The files are the same
			return isSame;
		}
		/// <summary>
		/// Copy a single file into target directory.  
		/// </summary>
		/// <param name="pSourceFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="pTargetDir">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="pSf">
		/// A <see cref="SettingFile"/>
		/// </param>
		private bool CopyFile (string pSourceFileName, string pTargetDir, SettingFile pSf)
		{
			//long len = 0;
			bool ret = false;
			//Construct a full target file name with path and name
			string targetFileName = System.IO.Path.GetFileName (pSourceFileName);
			targetFileName = System.IO.Path.Combine (pTargetDir, targetFileName);
			
			bool proceedToCopy = false;
			
			
			//If same file name exists in target location. If it is the same file, skip. Otherwise create a new file name.
			if (System.IO.File.Exists (targetFileName)) {
				Log ("...", false);
				if (IsSameFile (pSourceFileName, targetFileName)) {
					SameFileAction (pSf, pSourceFileName);
				} else {
					string ext = System.IO.Path.GetExtension (targetFileName);
					string onlyFileName = System.IO.Path.GetFileNameWithoutExtension (targetFileName);
					for (int i = 0; i < int.MaxValue; i++) {
						targetFileName = System.IO.Path.Combine (pTargetDir, string.Format ("{0}({1}){2}", onlyFileName, i, ext));
						if (System.IO.File.Exists (targetFileName)) {
							//If the new file name also exists
							if (IsSameFile (pSourceFileName, targetFileName)) {
								SameFileAction (pSf, pSourceFileName);
								break;
							}
							//If the target file is not the same file, continue trying a new file name
						} else {
							//New file name does not exist, proceed to copy
							proceedToCopy = true;
							Log ("new", false);
							break;
						}
					}
					
				}
				
				
			} else {
				//Proceed to copy file if target file does not exist
				proceedToCopy = true;
			}
			
			if (proceedToCopy) {
				
				ret = RealCopyFile (pSourceFileName, pSf, targetFileName);
			}
			return ret;
		}

		/// <summary>
		/// If the new file is the same as source file, do not copy. But if set to delete from memory card, we will delete the source.
		/// </summary>
		/// <param name="pSf"> A SettingFile </param>
		/// <param name="pSourceFileName"> A string </param>
		void SameFileAction (SettingFile pSf, string pSourceFileName)
		{
			if (pSf.DeleteAfterDownload) {
				Log ("remove source", false);
				try {
					System.IO.File.Delete (pSourceFileName);
				} catch (Exception ex) {
					SendAlert (string.Format ("Error trying to delete:\n{0}\nError:{1}", pSourceFileName, ex.Message));
					_aborting = true;
				}
			} else
				Log ("skip", false);
		}

		/// <summary>
		/// Copy a file. Overwrite if target exists. 
		/// </summary>
		/// <param name="pSourceFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="pDestFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// A <see cref="System.Int64"/>
		/// </returns>
		private bool BinaryFileCopy (string pSourceFileName, string pDestFileName)
		{
			byte[] buf;
			bool ret = true;
			using (FileStream fsr = new FileStream (pSourceFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
				//fileLen = fsr.Length;
				using (System.IO.FileStream fsw = new System.IO.FileStream (pDestFileName, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.ReadWrite)) {
					// Create the reader for data.
					using (BinaryReader r = new BinaryReader (fsr)) {
						// Create the writer for data.
						using (BinaryWriter w = new BinaryWriter (fsw)) {
							buf = r.ReadBytes(BUFFER_SIZE);
							while (buf.Length>0) {
								if (_aborting) 
								{
									ret = false;
									break;
								}
								w.Write(buf);
								buf = r.ReadBytes(BUFFER_SIZE);
								_totalCopied += buf.Length;
								ShowCopyProgresss();
								Thread.Sleep(0);
							}
							r.Close ();
							w.Close ();
							
						}
					}
				}
			}
			return ret;
		}
		
		/// <summary>
		/// Copy or move a file, returns file length 
		/// </summary>
		/// <param name="pSourceFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="pSf">
		/// A <see cref="SettingFile"/>
		/// </param>
		/// <param name="targetFileName">
		/// A <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// A <see cref="System.Int64"/> file length in bytes
		/// </returns>
		private bool RealCopyFile (string pSourceFileName, SettingFile pSf, string targetFileName)
		{
			bool ret = false;
			//DateTime createTime = System.IO.File.GetCreationTime (pSourceFileName);
			System.IO.FileInfo fi = new System.IO.FileInfo (pSourceFileName);
			DateTime createTime = fi.CreationTime;
			//long len = fi.Length;
			
			if (pSf.DeleteAfterDownload) {
				//delete each file after download
				try {
					//System.IO.File.Move (pSourceFileName, targetFileName);
					ret = BinaryFileCopy(pSourceFileName, targetFileName);
					if (ret)
						File.Delete(pSourceFileName);
					Log ("-moved", false);
				} catch (Exception ex) {
					SendAlert (string.Format ("Error trying to move:\n{0}\nto:\n{1}\nError:{2}", pSourceFileName, targetFileName, ex.Message));
					_aborting = true;
				}
			} else {
				//Just copy, no delete, always overwrite
				try {
					//System.IO.File.Copy (pSourceFileName, targetFileName, true);
					ret = BinaryFileCopy(pSourceFileName, targetFileName);
					Log ("-copied", false);
				} catch (Exception ex) {
					SendAlert (string.Format ("Error trying to copy:\n{0}\nto:\n{1}\nError:{2}", pSourceFileName, targetFileName, ex.Message));
					_aborting = true;
				}
			}
			System.IO.File.SetCreationTime (targetFileName, createTime);
			
			return ret;
		}
		private DateTime _startTime ;
		
		private void ShowCopyProgresss()
		{
			int speed = 0;
			System.TimeSpan diffResult = DateTime.Now - _startTime;
			double totalTime = diffResult.TotalMilliseconds;
			if (totalTime > 0)
				speed = (int)((float)_totalCopied / (float) totalTime * 1000);
			ProgressUpdate (_sumFiles, _totalFiles, _totalCopied, _totalSize, speed, true);
		}
		
		private int _sumFiles;
		
		/// <summary>
		/// Search source directory and its subfolders for files that matches pattern. 
		/// </summary>
		/// <param name="pSearchOnly">
		/// A <see cref="System.Boolean"/>
		/// </param>
		/// <param name="pSf">
		/// A <see cref="SettingFile"/>
		/// </param>
		private void DoSearch (bool pSearchOnly, SettingFile pSf)
		{
			string currentPath = pSf.SourceDir;
			
			_totalCopied = 0;
			_startTime = DateTime.Now;
			string[] mediaTypes = pSf.MediaTypes.Split (new char[] { ' ', ',', ';' });
			_sumFiles = 0;
			long sumSize = 0;
			List<string> fileList = new List<string>();
			foreach (string mediaType in mediaTypes) {
				string searchPattern = mediaType.Trim ();
				
				if (searchPattern != "") {
					//if (!pSearchOnly)
					Log (string.Format ("Searching pattern \"{0}\"\n\r", searchPattern), true);
					int patternFiles = 0;
					if (System.IO.Directory.Exists (currentPath)) {
						DirectoryInfo di = new DirectoryInfo(currentPath);
						foreach (FileInfo aFile in di.GetFiles(searchPattern, System.IO.SearchOption.AllDirectories))
						{
						//foreach (string aFile in System.IO.Directory.GetFiles (currentPath, searchPattern, System.IO.SearchOption.AllDirectories)) {
							if (!fileList.Contains(aFile.FullName))
							{
								fileList.Add(aFile.FullName);
								if (!pSearchOnly)
									Log (aFile.FullName, true);
								sumSize += aFile.Length;
								_sumFiles++;
								patternFiles++;
								//Do actual copy if it is not searchonly
								if (!pSearchOnly) {
									if (pSf.CreateSubDir) {
										//Create subdirectories.
										CopyWithSubDir (aFile.FullName, pSf.TargetDir, pSf);
									} else {
										//Do not create subdirectories. Just copy all file into flat target dir.
										CopyFile (aFile.FullName, pSf.TargetDir, pSf);
									}
									//_totalCopied += copied;
									
								} else
									//Send speed -1 to indicate it is search only
									ProgressUpdate (_sumFiles, _totalFiles, 0,0,-1, true);
								
								if (!pSearchOnly)
									Log ("\n", false);
							}								
							//yield cpu to other threads
							System.Threading.Thread.Sleep (0);
							
							if (_aborting) {
								Log ("Job aborted\n", true);
								break;

							}
						}
					} else {
						Log (string.Format ("Path \"{0}\" not found\n", currentPath), true);
					}
					Log (string.Format ("Found {0} files in pattern\n", patternFiles), true);
				}
				if (_aborting) {
					Log ("Search aborted\n", true);
					break;
				}
			}
			
			
			
			if (pSearchOnly) {
				_totalFiles = _sumFiles;
				_totalSize = sumSize;
				Log (string.Format ("Total {0} files.\n", _sumFiles.ToString ()), true);
			} else {
				Log (string.Format ("{0} files processed.\n", _sumFiles.ToString ()), true);
			}
		}


		/// <summary>
		/// Threaded download process. Make sure there is no direct calls to UI.
		/// </summary>
		private void Job ()
		{
			
			_totalFiles = 0;
			_totalSize = 0;
			SettingFile sf = new SettingFile ();
			if (!sf.Reload ()) {
				//If there is no setting file, don't proceed downlod.
				Log ("Setting file cannot be opened!", true);
				
			} else {
				if (!System.IO.Directory.Exists (sf.TargetDir)) {
					Log (string.Format ("Target dir \"{0}\" does not exist.", sf.TargetDir), true);
					
				} else {
					
					DoSearch (true, sf);
					//count total number of files only
					if (!_aborting) {
						ProgressUpdate (0, _totalFiles, 0,0, 0, true);
						
						//Create buffers to compare files. GC should collect these memories after this function ends
						_1kBuf0 = new byte[BUFFER_SIZE];
						_1kBuf1 = new byte[BUFFER_SIZE];
						
						DoSearch (false, sf);
						//do actual copies
					}
				}
			}
			
			DownloadComplete ();
		}

		/// <summary>
		/// Start job in a thread 
		/// </summary>
		public void RunAtBackground ()
		{
			//Set status as busy
			ProgressUpdate (0, 0, 0, 0,0, true);
			
			_aborting = false;
			
			Thread thr = new Thread (new ThreadStart (Job));
			thr.Start ();
			
		}

		private bool _aborting;
		/// <summary>
		/// Abort the job 
		/// </summary>
		public void Abort ()
		{
			_aborting = true;
		}
		
		// Define a delegate named LogHandler, which will encapsulate
		// any method that takes a string as the parameter and returns no value
		public delegate void LogHandlerDelegate (string pMessage, bool pWithTimeStamp);

		// Define an Event based on the above Delegate
		public event LogHandlerDelegate LogEvent;

		private void Log (string msg, bool pWithTimeStamp)
		{
			if (LogEvent != null)
				LogEvent (msg, pWithTimeStamp);
		}

		// Define a delegate to update progress.
		// PCurrent is current progress, start from 0
		// pMax is maximum progress.
		// pSpeed is bytes/second 
		public delegate void ProgressUpdateDelegate (int pCurrent, int pMax, long pCopied, long pTotalSize, int pSpeed, bool pIsBusy);

		// Define an Event based on the above Delegate
		public event ProgressUpdateDelegate ProgressUpdateEvent;

		private void ProgressUpdate (int pCurrent, int pMax, long pCopied, long pTotalSize, int pSpeed, bool pIsBusy)
		{
			if (ProgressUpdateEvent != null)
				ProgressUpdateEvent (pCurrent, pMax, pCopied, pTotalSize, pSpeed, pIsBusy);
		}

		// Define a delegate named LogHandler, 
		public delegate void DownloadCompleteHandlerDelegate ();

		// Define an Event based on the above Delegate
		public event DownloadCompleteHandlerDelegate DownloadCompleteEvent;

		private void DownloadComplete ()
		{
			if (DownloadCompleteEvent != null)
				DownloadCompleteEvent ();
		}

		//Use SendAlertHandler to popup a box on UI thread
		public delegate void SendAlertHandlerDelegate (string pMessage);
		public event SendAlertHandlerDelegate SendAlertEvent;
		private void SendAlert (string pMessage)
		{
			if (SendAlertEvent != null)
				SendAlertEvent (pMessage);
		}
	}
}

